// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using Microsoft.EntityFrameworkCore.Storage.Internal;

namespace Microsoft.EntityFrameworkCore.Storage;

/// <summary>
///     <para>
///         Creates instances of the <see cref="IRelationalValueBufferFactory" /> type. <see cref="IRelationalValueBufferFactory" />
///         instances are tied to a specific result shape. This factory is responsible for creating the
///         <see cref="IRelationalValueBufferFactory" /> for a given result shape.
///     </para>
///     <para>
///         This type is typically used by database providers (and other extensions). It is generally
///         not used in application code.
///     </para>
/// </summary>
/// <remarks>
///     <para>
///         This factory results in value buffers that use they strongly typed APIs to read back individual values from the
///         underlying <see cref="DbDataReader" />.
///     </para>
///     <para>
///         The service lifetime is <see cref="ServiceLifetime.Singleton" />. This means a single instance
///         is used by many <see cref="DbContext" /> instances. The implementation must be thread-safe.
///         This service cannot depend on services registered as <see cref="ServiceLifetime.Scoped" />.
///     </para>
///     <para>
///         See <see href="https://aka.ms/efcore-docs-providers">Implementation of database providers and extensions</see>
///         for more information and examples.
///     </para>
/// </remarks>
public class TypedRelationalValueBufferFactoryFactory : IRelationalValueBufferFactoryFactory
{
    /// <summary>
    ///     The parameter representing the DbDataReader in generated expressions.
    /// </summary>
    public static readonly ParameterExpression DataReaderParameter
        = Expression.Parameter(typeof(DbDataReader), "dataReader");

    private static readonly MethodInfo GetFieldValueMethod =
        typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetFieldValue), new[] { typeof(int) })!;

    private static readonly MethodInfo IsDbNullMethod =
        typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.IsDBNull), new[] { typeof(int) })!;

    private static readonly MethodInfo ThrowReadValueExceptionMethod
        = typeof(TypedRelationalValueBufferFactoryFactory).GetTypeInfo().GetDeclaredMethod(nameof(ThrowReadValueException))!;

    /// <summary>
    ///     Initializes a new instance of the <see cref="TypedRelationalValueBufferFactoryFactory" /> class.
    /// </summary>
    /// <param name="dependencies">Parameter object containing dependencies for this service.</param>
    public TypedRelationalValueBufferFactoryFactory(RelationalValueBufferFactoryDependencies dependencies)
    {
        Dependencies = dependencies;
    }

    /// <summary>
    ///     Relational provider-specific dependencies for this service.
    /// </summary>
    protected virtual RelationalValueBufferFactoryDependencies Dependencies { get; }

    private readonly struct CacheKey : IEquatable<CacheKey>
    {
        public CacheKey(IReadOnlyList<TypeMaterializationInfo> materializationInfo)
        {
            TypeMaterializationInfo = materializationInfo;
        }

        public IReadOnlyList<TypeMaterializationInfo> TypeMaterializationInfo { get; }

        public override bool Equals(object? obj)
            => obj is CacheKey cacheKey && Equals(cacheKey);

        public bool Equals(CacheKey other)
            => TypeMaterializationInfo.SequenceEqual(other.TypeMaterializationInfo);

        public override int GetHashCode()
        {
            var hash = new HashCode();
            // ReSharper disable once ForCanBeConvertedToForeach
            for (var i = 0; i < TypeMaterializationInfo.Count; i++)
            {
                hash.Add(TypeMaterializationInfo[i]);
            }

            return hash.ToHashCode();
        }
    }

    private readonly ConcurrentDictionary<CacheKey, TypedRelationalValueBufferFactory> _cache = new();

    /// <summary>
    ///     Creates a new <see cref="IRelationalValueBufferFactory" />.
    /// </summary>
    /// <param name="types">Types and mapping for the values to be read.</param>
    /// <returns>The newly created <see cref="IRelationalValueBufferFactoryFactory" />.</returns>
    public virtual IRelationalValueBufferFactory Create(IReadOnlyList<TypeMaterializationInfo> types)
        => _cache.GetOrAdd(
            new CacheKey(types),
            k => new TypedRelationalValueBufferFactory(
                Dependencies,
                CreateArrayInitializer(k, Dependencies.CoreOptions.AreDetailedErrorsEnabled)));

    private static Func<DbDataReader, object[]> CreateArrayInitializer(CacheKey cacheKey, bool detailedErrorsEnabled)
        => Expression.Lambda<Func<DbDataReader, object[]>>(
                Expression.NewArrayInit(
                    typeof(object),
                    cacheKey.TypeMaterializationInfo
                        .Select(
                            (mi, i) =>
                                CreateGetValueExpression(
                                    DataReaderParameter,
                                    i,
                                    mi,
                                    detailedErrorsEnabled))),
                DataReaderParameter)
            .Compile();

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static TValue ThrowReadValueException<TValue>(
        Exception exception,
        object? value,
        IPropertyBase? property = null)
    {
        var expectedType = typeof(TValue);
        var actualType = value?.GetType();

        string message;

        if (property != null)
        {
            var entityType = property.DeclaringType.DisplayName();
            var propertyName = property.Name;

            message
                = exception is NullReferenceException
                || Equals(value, DBNull.Value)
                    ? RelationalStrings.ErrorMaterializingPropertyNullReference(entityType, propertyName, expectedType)
                    : exception is InvalidCastException
                        ? CoreStrings.ErrorMaterializingPropertyInvalidCast(entityType, propertyName, expectedType, actualType)
                        : RelationalStrings.ErrorMaterializingProperty(entityType, propertyName);
        }
        else
        {
            message
                = exception is NullReferenceException
                    ? RelationalStrings.ErrorMaterializingValueNullReference(expectedType)
                    : exception is InvalidCastException
                        ? RelationalStrings.ErrorMaterializingValueInvalidCast(expectedType, actualType)
                        : RelationalStrings.ErrorMaterializingValue;
        }

        throw new InvalidOperationException(message, exception);
    }

    private static Expression CreateGetValueExpression(
        Expression dataReaderExpression,
        int index,
        TypeMaterializationInfo materializationInfo,
        bool detailedErrorsEnabled,
        bool box = true)
    {
        var getMethod = materializationInfo.Mapping.GetDataReaderMethod();
        var indexExpression = Expression.Constant(index);

        Expression valueExpression
            = Expression.Call(
                getMethod.DeclaringType != typeof(DbDataReader)
                    ? Expression.Convert(dataReaderExpression, getMethod.DeclaringType!)
                    : dataReaderExpression,
                getMethod,
                indexExpression);

        valueExpression = materializationInfo.Mapping.CustomizeDataReaderExpression(valueExpression);

        var converter = materializationInfo.Mapping.Converter;

        if (converter != null)
        {
            if (valueExpression.Type != converter.ProviderClrType)
            {
                valueExpression = Expression.Convert(valueExpression, converter.ProviderClrType);
            }

            valueExpression = ReplacingExpressionVisitor.Replace(
                converter.ConvertFromProviderExpression.Parameters.Single(),
                valueExpression,
                converter.ConvertFromProviderExpression.Body);
        }

        if (valueExpression.Type != materializationInfo.ModelClrType)
        {
            valueExpression = Expression.Convert(valueExpression, materializationInfo.ModelClrType);
        }

        var exceptionParameter
            = Expression.Parameter(typeof(Exception), name: "e");

        var property = materializationInfo.Property;

        if (detailedErrorsEnabled)
        {
            var catchBlock
                = Expression
                    .Catch(
                        exceptionParameter,
                        Expression.Call(
                            ThrowReadValueExceptionMethod
                                .MakeGenericMethod(valueExpression.Type),
                            exceptionParameter,
                            Expression.Call(
                                dataReaderExpression,
                                GetFieldValueMethod.MakeGenericMethod(typeof(object)),
                                indexExpression),
                            Expression.Constant(property, typeof(IPropertyBase))));

            valueExpression = Expression.TryCatch(valueExpression, catchBlock);
        }

        if (box && valueExpression.Type.IsValueType)
        {
            valueExpression = Expression.Convert(valueExpression, typeof(object));
        }

        if (materializationInfo.IsNullable != false)
        {
            Expression replaceExpression;
            if (converter?.ConvertsNulls == true)
            {
                replaceExpression = ReplacingExpressionVisitor.Replace(
                    converter.ConvertFromProviderExpression.Parameters.Single(),
                    Expression.Default(converter.ProviderClrType),
                    converter.ConvertFromProviderExpression.Body);

                if (replaceExpression.Type != valueExpression.Type)
                {
                    replaceExpression = Expression.Convert(replaceExpression, valueExpression.Type);
                }
            }
            else
            {
                replaceExpression = Expression.Default(valueExpression.Type);
            }

            valueExpression
                = Expression.Condition(
                    Expression.Call(dataReaderExpression, IsDbNullMethod, indexExpression),
                    replaceExpression,
                    valueExpression);
        }

        return valueExpression;
    }
}
